package com.bowen.paycenter.controller;

import cn.hutool.core.convert.ConverterRegistry;
import cn.hutool.core.date.DateUtil;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.domain.AlipayTradeAppPayModel;
import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradeAppPayRequest;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.response.AlipayTradeAppPayResponse;
import com.alipay.api.response.AlipayTradeRefundResponse;
import com.bowen.paycenter.config.AliPayConfig;
import com.bowen.paycenter.config.AlipayProperties;
import com.bowen.paycenter.domain.PayInfo;
import com.bowen.paycenter.resultResponse.ApiResult;
import com.bowen.paycenter.service.IPayService;
import com.google.gson.Gson;
import constant.AliPayConstant;
import constant.PayStatusConstant;
import constant.PayTypeConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.rmi.ServerException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * @ClassName AliPayController
 * @Description 阿里云支付
 * @Date 2020/12/3 11:31
 * @Author Rick Jen
 */
@RestController
@RequestMapping(value = "/pay/aliPay")
@Slf4j
public class AliPayController {

    private static final ConverterRegistry CONVERTER_REGISTRY = ConverterRegistry.getInstance();

    private final static String QUEUE_PAY_NOTIFY = "payNotify";

    /**
     * 支付宝支付配置
     */
    private final AlipayProperties alipayProperties;

    /**
     * 支付宝配置
     */
    private final AliPayConfig aliPayConfig;

    private final IPayService payService;

    private final AmqpTemplate amqpTemplate;

    public AliPayController(AlipayProperties alipayProperties, AliPayConfig aliPayConfig, IPayService payService, AmqpTemplate amqpTemplate) {
        this.alipayProperties = alipayProperties;
        this.aliPayConfig = aliPayConfig;
        this.payService = payService;
        this.amqpTemplate = amqpTemplate;
    }

    /**
     * @Description 支付宝支付
     * @author Rick Jen
     * @Date 2020/12/3 11:43
     * @Param orderId 订单号 建议雪花算法生成
     * @Param amount 订单金额
     * @Param transactionDescription 交易描述
     * @Param goodsName 商品标题
     * @Return
     * @Exception
     */
    @RequestMapping(value = "/toPayByAli", method = RequestMethod.POST)
    @Transactional(rollbackFor = Exception.class)
    public ApiResult toPayByAli(@RequestParam("orderId") String orderId,
                                @RequestParam("amount") BigDecimal amount,
                                @RequestParam("transactionDescription") String transactionDescription,
                                @RequestParam("goodsName") String goodsName
    ) throws AlipayApiException {
        // 创建支付订单
        int flag = payService.createPayInfo(PayInfo.builder()
                .orderNo(CONVERTER_REGISTRY.convert(Long.class, orderId))
                .payPlatform(PayTypeConstant.ALI_PAY)
                .platformStatus(PayStatusConstant.UN_PAID)
                .payAmount(amount)
                .refundStatus(PayStatusConstant.DONT_REFUND)
                .refundAmount(BigDecimal.ZERO)
                .createTime(DateUtil.date())
                .updateTime(DateUtil.date())
                .build());
        if (flag < 1) {
            throw new RuntimeException("支付宝下单失败！");
        }
        //构造client
        AlipayClient alipayClient = aliPayConfig.alipayClient();
        //实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称：alipay.trade.app.pay
        AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
        request.setBizModel(createAlipayModel(orderId, amount, transactionDescription, goodsName));
        request.setNotifyUrl(alipayProperties.getNotifyUrl() + "/pay/aliPay/notifyAliPay");
        try {
            //这里和普通的接口调用不同，使用的是sdkExecute
            AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
            //就是orderString 可以直接给客户端请求，无需再做处理。
            log.info("支付宝支付结果：{{}}", response.getBody());
            return ApiResult.success("操作成功！", response.getBody());
        } catch (AlipayApiException e) {
            e.printStackTrace();
            return ApiResult.error(e.getMessage());
        }
    }

    /**
     * 创建支付宝支付请求体
     *
     * @return
     * @Param orderId 订单号 建议雪花算法生成
     * @Param amount 订单金额
     * @Param transactionDescription 交易描述
     * @Param goodsName 商品标题
     */
    private AlipayTradeAppPayModel createAlipayModel(String orderId, BigDecimal amount, String transactionDescription, String goodsName) {
        //SDK已经封装掉了公共参数，这里只需要传入业务参数。以下方法为sdk的model入参方式(model和biz_content同时存在的情况下取biz_content)。
        AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
        // 对一笔交易的具体描述信息。如果是多种商品，请将商品描述字符串累加传给body。
        model.setBody(transactionDescription);
        // 商品的标题/交易标题/订单标题/订单关键字等。
        model.setSubject(goodsName);
        // 商户网站唯一订单号  建议使用雪花算法生成
        model.setOutTradeNo(orderId);
        // 该笔订单允许的最晚付款时间，逾期将关闭交易。取值范围：5m～15d。m-分钟，h-小时，d-天，1c-当天（1c-当天的情况下，无论交易何时创建，都在0点关闭）。 该参数数值不接受小数点， 如 1.5h，可转换为 90m。
        model.setTimeoutExpress("30m");
        // 支付金额
        model.setTotalAmount(CONVERTER_REGISTRY.convert(String.class, amount));
        model.setProductCode("QUICK_MSECURITY_PAY");
        return model;
    }

    /**
     * @Description 阿里云支付回调
     * @author Rick Jen
     * @Date 2020/12/3 15:30
     * @Param
     * @Return
     * @Exception
     */
    @RequestMapping(value = "/notifyAliPay", method = RequestMethod.POST)
    public String notifyAliPay(HttpServletRequest request) throws FileNotFoundException, UnsupportedEncodingException, ServerException {
        // 一定要验签，防止黑客篡改参数
        Map<String, String[]> parameterMap = request.getParameterMap();
        StringBuilder notifyBuild = new StringBuilder("/****************************** 支付宝支付回调 ******************************/\n");
        parameterMap.forEach((key, value) -> notifyBuild.append(key).append("=").append(value[0]).append("\n"));
        log.info(notifyBuild.toString());
        log.info("通知回调：AlipayController-notify-日志信息信息：" + request.getParameterMap().toString());

        boolean flag = rsaCheckV1(request);
        if (flag) {
            /*
             *
             * 商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号， 并判断total_amount是否确实为该订单的实际金额（即商户订单创建时的金额）， 同时需要校验通知中的seller_id（或者seller_email)
             * 是否为out_trade_no这笔单据的对应的操作方（有的时候，一个商户可能有多个seller_id/seller_email），
             *
             * 上述有任何一个验证不通过，则表明本次通知是异常通知，务必忽略。 在上述验证通过后商户必须根据支付宝不同类型的业务通知，正确的进行不同的业务处理，并且过滤重复的通知结果数据。
             * 在支付宝的业务通知中，只有交易通知状态为TRADE_SUCCESS或TRADE_FINISHED时，支付宝才会认定为买家付款成功。
             */

            // 交易状态
            String tradeStatus = new String(request.getParameter("trade_status").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            // 商户订单号
            String outTradeNo = new String(request.getParameter("out_trade_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            // 支付宝交易号
            String tradeNo = new String(request.getParameter("trade_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            // 付款金额
            String totalAmount = new String(request.getParameter("total_amount").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            String buyerLogonId = new String(request.getParameter("buyer_logon_id").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            // TRADE_FINISHED(表示交易已经成功结束，并不能再对该交易做后续操作);
            // TRADE_SUCCESS(表示交易已经成功结束，可以对该交易做后续操作，如：分润、退款等);
            if (AliPayConstant.TRADE_SUCCESS.equals(tradeStatus)) {
                //TODO 交易成功->执行操作
                log.info("交易状态:{{}}", tradeStatus);
                log.info("商户订单号:{{}}", outTradeNo);
                log.info("支付宝交易号:{{}}", tradeNo);
                log.info("付款金额:{{}}", totalAmount);
                log.info("buyerLogonId:{{}}", buyerLogonId);
                // 查询订单信息
                PayInfo payInfo = payService.queryByOrderId(outTradeNo);
                if (payInfo == null) {
                    // 警告 可发送通知给相关人员
                    throw new RuntimeException("未查询到下单信息，但收到了回调请求！");
                }
                if (payInfo.getPayAmount().compareTo(CONVERTER_REGISTRY.convert(BigDecimal.class, totalAmount)) != 0) {
                    // 警告 可发送通知给相关人员
                    throw new RuntimeException("下单支付金额与回调金额不一致！");
                }
                // 修改订单支付状态
                payInfo.setPlatformStatus(PayStatusConstant.PAID);
                payInfo.setPlatformNumber(tradeNo);
                payService.updatePayInfo(payInfo);
                //TODO pay发送MQ消息，mall接受MQ消息
                amqpTemplate.convertAndSend(QUEUE_PAY_NOTIFY, new Gson().toJson(payInfo));
                // 返回success 让支付宝官方不再发送回调请求
                return "success";
            }
            return "fail";
        }
        // 验签失败 可发送短信或通知给相关人员
        return "fail";
    }

    /**
     * 支付宝全额退款
     *
     * @param outTradeNo 商户订单号
     * @return
     * @throws AlipayApiException
     */
    @RequestMapping(value = "/refundByAliPay", method = RequestMethod.POST)
    public ApiResult refundByAliPay(String outTradeNo) throws AlipayApiException {
        // 查询订单信息
        PayInfo payInfo = payService.queryByOrderId(outTradeNo);
        if (payInfo == null) {
            return ApiResult.error("未查询到订单信息！");
        }
        //构造client
        AlipayClient alipayClient = aliPayConfig.alipayClient();
        AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
        AlipayTradeRefundModel model = new AlipayTradeRefundModel();
        model.setTradeNo(payInfo.getPlatformNumber());
        model.setOutTradeNo(outTradeNo);
        model.setRefundAmount(CONVERTER_REGISTRY.convert(String.class, payInfo.getPayAmount()));
        model.setRefundReason("退款");
        refundRequest.setBizModel(model);

        try {
            AlipayTradeRefundResponse response = alipayClient.certificateExecute(refundRequest);

            if (response.isSuccess() && AliPayConstant.SUCCESS.equalsIgnoreCase(response.getFundChange())) {
                // 成功
                return ApiResult.success("操作成功！", response);
            } else {
                // 失败
                return ApiResult.error("支付宝退款失败！");
            }
        } catch (AlipayApiException e) {
            e.printStackTrace();
            log.error(" - 支付宝退款异常 - ", e);
            return ApiResult.error("支付宝退款失败！");
        }
    }

    /**
     * 校验签名 证书模式
     *
     * @param request
     * @return
     */
    public boolean rsaCheckV1(HttpServletRequest request) throws FileNotFoundException {
        // 获取支付宝POST过来反馈信息
        Map<String, String> params = new HashMap<>();
        Map requestParams = request.getParameterMap();
        for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
            String name = (String) iter.next();
            String[] values = (String[]) requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
            }
            // 乱码解决，这段代码在出现乱码时使用。
            valueStr = new String(valueStr.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            params.put(name, valueStr);
        }
        // 切记alipaypublickey是支付宝的公钥，请去open.alipay.com对应应用下查看。
        // boolean AlipaySignature.rsaCertCheckV1(Map<String, String> params, String publicKeyCertPath, String charset,String signType)
        try {
            return AlipaySignature.rsaCertCheckV1(params, ResourceUtils.getFile(alipayProperties.getAlipayPublicCertPath()).getAbsolutePath(), alipayProperties.getCharset(),
                    alipayProperties.getSignType());
        } catch (AlipayApiException e) {
            e.printStackTrace();
            return false;
        }
    }
}
